package com.atguigu.userprofile.service.impl;

import com.alibaba.fastjson.JSON;
import com.atguigu.userprofile.bean.TagCondition;
import com.atguigu.userprofile.bean.TagInfo;
import com.atguigu.userprofile.bean.UserGroup;
import com.atguigu.userprofile.constants.ConstCodes;
import com.atguigu.userprofile.mapper.UserGroupMapper;
import com.atguigu.userprofile.service.TagInfoService;
import com.atguigu.userprofile.service.UserGroupService;
import com.atguigu.userprofile.utils.RedisUtil;
import com.baomidou.dynamic.datasource.annotation.DS;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;

import org.apache.catalina.User;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import redis.clients.jedis.Jedis;

import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author zhangchen
 * @since 2021-05-04
 */
@Service
@Slf4j
@DS("mysql")
public class UserGroupServiceImpl extends ServiceImpl<UserGroupMapper, UserGroup> implements UserGroupService {

    @Autowired
    private TagInfoService tagInfoService;

    @Override
    public void saveUserGroupToMysql(UserGroup userGroup) {

        //把条件由List转为 json
        List<TagCondition> tagConditions = userGroup.getTagConditions();
        String tagConditionsJson = JSON.toJSONString(tagConditions);
        userGroup.setConditionJsonStr(tagConditionsJson);

        //生成方便查看的查询条件
        String str = userGroup.conditionJsonToComment();
        userGroup.setConditionComment(str);

        //写入mysql
        //这里无需声明一个 UserGroupMapper，从父类中直接获取 或者直接调用service中提供的保存方法
        saveOrUpdate(userGroup);

    }

    @Override
    public void saveUserGroupToCK(UserGroup userGroup) {

        String sql = generateBitmapAndSql(userGroup, userGroup.getBusiDate());

        /*
                如果是普通的增删改查，可以直接调用MybatisPlus。
                求交集的sql，只能手动编写方法，调用dao写入ck
         */
        super.baseMapper.saveUserGroupToCK(userGroup.getId().toString(),sql);


        System.out.println(sql);

    }

    /*
            保存分群信息到redis
     */
    @Override
    public UserGroup saveUserGroupToRedis(UserGroup userGroup) {

        String id = userGroup.getId().toString();

        //1.先查询之前的分群信息   完成更新人群包功能时，过时
        //List<String> uids = super.baseMapper.queryUserGroupFromCK(id);

        //改为直接计算
        String sql = generateBitmapAndSql(userGroup, userGroup.getBusiDate());

        List<String> uids = super.baseMapper.calUserGroupFromCK(sql);


        String [] uidsArray = new String []{};

        uidsArray = uids.toArray(uidsArray);

        /*
            2.设计数据在redis存储的K-V

            K：  user_group:id

            V：  多值类型， list(允许重复),set(不重复),zset(排序)
                 set提供了一个api :  sismember，可以判断一个元素是否在某个集合中。
                                    后续有需求，判断一个人是否符合某个人群

         */
        String key = "user_group:" + id;

        Jedis jedis = RedisUtil.getJedis();

        //先删除
        jedis.del(key);
        // 3.写入
        jedis.sadd(key,uidsArray);

        jedis.close();

        System.out.println("当前分群的人数:"+uids.size());

        //把分群的人数加进去
        userGroup.setUserGroupNum(uids.size() + 0l);

        return userGroup;

    }

    /*
            预估分群人数
     */
    @Override
    public Long evaluateUserGroup(UserGroup userGroup) {

        String sql = generateBitmapAndSql(userGroup, userGroup.getBusiDate());

        //查询上述sql计算的人群包的大小
        Long userGroupNum = super.baseMapper.evaluateUserGroupNum(sql);

        return userGroupNum;
    }

    /*
        更新ck中的人群包
     */
    @Override
    public UserGroup updateUserGroupInCk(String busiDate, String id) {

        //删除之前这个id的人群包信息
        super.baseMapper.deleteUserGroupById(id);


        //根据id，查询mysql，得到 UserGroup对象，调用之前的saveUserGroupToCK(UserGroup userGroup)
        UserGroup userGroup = getById(id);

        //把重要的属性设置进去
        userGroup.setBusiDate(busiDate);

        List<TagCondition> tagConditions = JSON.parseArray(userGroup.getConditionJsonStr(), TagCondition.class);
        userGroup.setTagConditions(tagConditions);

        //重新计算当前 busiDate 的人群包，写入ck
        saveUserGroupToCK(userGroup);

        return userGroup;


    }

    /*
             bitmapAnd(
                        bitmapAnd(
                            ( sql1 ),(sql2)
                        ), ( sql3 )
                   )
                .....

             数量关系:   TagCondition的数量 - 1 = bitmapAnd的数量
     */
    public String generateBitmapAndSql(UserGroup userGroup,String do_date){

        //获取所有三级标签的map集合
        Map<String, TagInfo> tagInfoMap = tagInfoService.getTagInfoMapWithCode();

        //查询前台传入的所有条件TagCondition
        List<TagCondition> tagConditions = userGroup.getTagConditions();

        //准备一个最终的字符串
        StringBuilder builder = new StringBuilder();

        //生成sql
        for (TagCondition tagCondition : tagConditions) {

            //生产单个条件所查询的sql
            String singleSql = generateSingleTagConditionSql(tagCondition, tagInfoMap, do_date);

            //如果是第一个sql，是不拼接 bitmapAnd，因为是两两拼接。 等待第二个以后，每一个都拼接
            if (builder.length() == 0){

                builder.append("(")
                       .append(singleSql)
                       .append(")");

            }else{

                builder.insert(0," bitmapAnd(")
                       .append(",(")
                       .append(singleSql)
                       .append("))");

            }

        }

        return builder.toString();


    }

    /*
        select  groupBitmapMergeState(us) from user_tag_value_string where tag_code = 'tag_person_nature_gender' and tag_value in ( '男性','女性'  ) and dt='2020-06-14'

            user_tag_value_string: 变量。应该根据查询的tag的 tagValueType 确定从ck的哪张表去查
                    举例:  tag_person_nature_gender 的  tagValueType 是 string , 在ck的 user_tag_value_string

                    队友已经开发了:  TagInfoServiceImpl.getTagInfoMapWithCode,可以查询所有的三级标签。
                            Map<String,TagInfo>：  key是 tagCode

            tag_person_nature_gender： 变量。 前台封装的条件，是查询哪个标签的值

            in ( '男性','女性'  )： 变量。
                    in： 前台传入的操作运行符号。
                            如果运算符是in 或 not in，手动拼 ()

                    '男性','女性'： 前台传入的标签的值tagValues。如果标签值是string类型或date类型，手动加单引号。
                                                    如果是数字long ,decimal，无需处理。

            dt='2020-06-14'： 页面传入日期
     */
    public String generateSingleTagConditionSql(TagCondition tagCondition,Map<String,TagInfo> tagInfoMap,String do_date){

        String template = "select  groupBitmapMergeState(us) from %s where tag_code = '%s' and tag_value %s %s and dt='%s'";

        //确认第一个占位符 。 从哪张表去查
        String tagCode = tagCondition.getTagCode();
        TagInfo tagInfo = tagInfoMap.get(tagCode);
        String tagValueType = tagInfo.getTagValueType();

        String ckSoureTable = null;

        switch (tagValueType){
            case ConstCodes.TAG_VALUE_TYPE_DATE :  ckSoureTable = "user_tag_value_date"; break;
            case ConstCodes.TAG_VALUE_TYPE_LONG :  ckSoureTable = "user_tag_value_long"; break;
            case ConstCodes.TAG_VALUE_TYPE_STRING :  ckSoureTable = "user_tag_value_string"; break;
            case ConstCodes.TAG_VALUE_TYPE_DECIMAL :  ckSoureTable = "user_tag_value_decimal"; break;
        }

        //哪个标签
        String tag = tagCode.toLowerCase();

        //生成in ( '男性','女性'  )
        //操作符  operator": "in"  operator": "eq",
        String operator = tagCondition.getOperator();
        String sqlOperator = getSqlOperator(operator);

        List<String> tagValues = tagCondition.getTagValues();
        String conditionSql = "";
        if (tagValueType.equals(ConstCodes.TAG_VALUE_TYPE_STRING) || tagValueType.equals(ConstCodes.TAG_VALUE_TYPE_DATE)){

            conditionSql =  "'" + StringUtils.join(tagValues,"','") + "'";
        }else{
            conditionSql = StringUtils.join(tagValues,",");
        }

        //如果是in 或 not in ，在conditionSql前后加()
        if ("in".equals(operator) || "nin".equals(operator)){
            conditionSql = "(" + conditionSql +")";
        }

        //格式化语句
        String singleConditionSql = String.format(template, ckSoureTable, tag, sqlOperator, conditionSql, do_date);

        System.out.println(singleConditionSql);

        return singleConditionSql;
    }

    public String getSqlOperator(String operator){

        switch (operator) {
            case "eq" : return "=";
            case "neq" : return "<>";
            case "gte" : return ">=";
            case "lte" : return "<=";
            case "gt" : return ">";
            case "lt" : return "<";
            case "in" : return "in";
            case "nin" : return "not in";
        }
        throw new RuntimeException("没有匹配的操作符!");

    }
}
